﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using Pfz.Extensions;

namespace Pfz.RemoteGaming.Internal
{
	internal static class _AssemblyGenerator
	{
		internal static Dictionary<PropertyInfo, RemoteGameProperty> _propertiesDictionary = new Dictionary<PropertyInfo, RemoteGameProperty>();

		private static Type _ImplementType(Type oldType)
		{
			if (!oldType.IsAbstract)
				throw new RemoteGameException("Type " + oldType.FullName + " must be abstract.");

			if (!oldType.IsSubclassOf(typeof(RemoteGameComponent)))
				throw new RemoteGameException("Type " + oldType.FullName + " must inherit from RemoteGameComponent.");

			var methods = oldType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
			var newType = _DynamicModule.DefineType(oldType.FullName, TypeAttributes.Class | TypeAttributes.Sealed | TypeAttributes.Public | TypeAttributes.Serializable, oldType, null);
			foreach(var method in methods)
			{
				if (!method.IsAbstract)
					continue;

				if (!method.IsSpecialName)
					throw new RemoteGameException("Method " + method.Name + " of type " + oldType.FullName + " is abstract. Only properties are auto-implemented.");

				var name = method.Name;
				if (name.StartsWith("get_"))
					_ImplementGetMethod(method, newType);
				else
				if (name.StartsWith("set_"))
					_ImplementSetMethod(method, newType);
				else
					throw new RemoteGameException("Can't implement method " +  method.Name + " of type " + oldType.FullName + ". Unknown method type.");
			}

			return newType.CreateType();
		}

		private static RemoteGameProperty _GetRemotePropertyInfo(Type type, string name)
		{
			var property = type.GetProperty(name);
			if (property == null)
				throw new RemoteGameException("Can't find property " + name + " in type " + type.FullName);
			
			RemoteGameProperty result;
			bool found;
			lock(_propertiesDictionary)
				found = _propertiesDictionary.TryGetValue(property, out result);

			if (!found)
				throw new RemoteGameException("The property " + name + " of type " + type.FullName + " is not registered. Did you register the common assemblies right?");

			return result;
		}

		internal static void _GetOrCreateRemotePropertyInfo(PropertyInfo property)
		{
			RemoteGameProperty remoteProperty;
			lock (_propertiesDictionary)
			{
				if (_propertiesDictionary.ContainsKey(property))
					return;

				{
					int id = _propertiesDictionary.Count;
					bool isClientProperty = property.ContainsCustomAttribute<ClientGamePropertyAttribute>();
					bool isVolatileProperty = false;
					
					if (!isClientProperty) // at this moment, client properties are never considered volatile.
						isVolatileProperty = property.ContainsCustomAttribute<VolatileGamePropertyAttribute>();

					remoteProperty = new RemoteGameProperty(id, property.DeclaringType, property.PropertyType, property.Name, isClientProperty, isVolatileProperty);
					_propertiesDictionary.Add(property, remoteProperty);
				}
			}

			RemoteGameProperty._propertiesLock.EnterWriteLock();
			try
			{
				RemoteGameProperty._properties.Add(remoteProperty);
			}
			finally
			{
				RemoteGameProperty._propertiesLock.ExitWriteLock();
			}
		}

		private static MethodInfo _getValueByPropertyId = ReflectionHelper.GetMethod(() => RemoteGameProperty.GetPropertyValueById<int>(0, null)).GetGenericMethodDefinition();
		private static void _ImplementGetMethod(MethodInfo method, TypeBuilder newType)
		{
			if (method.GetParameters().Length > 0)
				throw new RemoteGameException("Method " + method.Name + " of type " + newType.FullName + " has parameters.");

			var methodAttributes = method.Attributes;
			methodAttributes &= ~MethodAttributes.Abstract;
			methodAttributes |= MethodAttributes.Final;
			var newMethod = newType.DefineMethod(method.Name, methodAttributes, method.ReturnType, null);
			var generator = newMethod.GetILGenerator();
			generator.EmitLoadInt32(_GetRemotePropertyInfo(method.DeclaringType, method.Name.Substring(4))._id);
			generator.EmitLoadArgument(0);
			generator.EmitCall(OpCodes.Call, _getValueByPropertyId.MakeGenericMethod(method.ReturnType), null);
			generator.EmitReturn();

			newType.DefineMethodOverride(newMethod, method);
			_GetRemotePropertyInfo(method.DeclaringType, method.Name.Substring(4));
		}

		private static MethodInfo _setValueByPropertyId = ReflectionHelper.GetMethod(() => RemoteGameProperty.SetPropertyValueById(0, null, 0)).GetGenericMethodDefinition();
		private static void _ImplementSetMethod(MethodInfo method, TypeBuilder newType)
		{
			var parameterTypes = method.GetParameterTypes();
			if (parameterTypes.Length != 1)
				throw new RemoteGameException("Method " + method.Name + " of type " + newType.FullName + " has a parameter count different of 1.");

			var methodAttributes = method.Attributes;
			methodAttributes &= ~MethodAttributes.Abstract;
			methodAttributes |= MethodAttributes.Final;
			var newMethod = newType.DefineMethod(method.Name, methodAttributes, method.ReturnType, parameterTypes);
			var generator = newMethod.GetILGenerator();
			generator.EmitLoadInt32(_GetRemotePropertyInfo(method.DeclaringType, method.Name.Substring(4))._id);
			generator.EmitLoadArgument(0);
			generator.EmitLoadArgument(1);
			generator.EmitCall(OpCodes.Call, _setValueByPropertyId.MakeGenericMethod(parameterTypes[0]), null);
			generator.EmitReturn();

			newType.DefineMethodOverride(newMethod, method);
			_GetRemotePropertyInfo(method.DeclaringType, method.Name.Substring(4));
		}

		private static Dictionary<Type, ConstructorInfo> _alreadyGenerated = new Dictionary<Type, ConstructorInfo>();

		internal static ConstructorInfo GetConstructor(Type type)
		{
			if (type == null)
				throw new ArgumentNullException("type");

			ConstructorInfo result;
			lock(_alreadyGenerated)
			{
				if (_alreadyGenerated.TryGetValue(type, out result))
					return result;

				result = _ImplementType(type).GetConstructor(Type.EmptyTypes);
				if (result == null)
					throw new RemoteGameException("Error generating the needed type.");

				_alreadyGenerated.Add(type, result);
			}

			return result;
		}
	}
}
